/*
 * ParseGenBank: A minimalistic event driven GenBank file parser in C++.
 *
 * Copyright (C) 2007 Andrew Miller
 * 
 * Permission is hereby granted, free of charge, to any person
 * obtaining a copy of this software and associated documentation
 * files (the "Software"), to deal in the Software without
 * restriction, including without limitation the rights to use,
 * copy, modify, merge, publish, distribute, sublicense, and/or sell
 * copies of the Software, and to permit persons to whom the
 * Software is furnished to do so, subject to the following
 * conditions:
 * 
 * The above copyright notice and this permission notice shall be
 * included in all copies or substantial portions of the Software.
 * 
 * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND,
 * EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES
 * OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND
 * NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT
 * HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY,
 * WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING
 * FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR
 * OTHER DEALINGS IN THE SOFTWARE.
 */

#include "GenbankParser.hpp"
#include <fcntl.h>
#include <unistd.h>
#include <stdlib.h>
#include <sys/errno.h>
#include <sstream>

#if defined(__GNUC__) && (__GNUC__ > 2)
#define LIKELY(x) __builtin_expect(!!(x), 1)
#define UNLIKELY(x) __builtin_expect(x, 0)
#else
#define LIKELY(x) x
#define UNLIKELY(x) x
#endif

IOException::IOException(const char* aMessage) throw()
  : mMessage(aMessage)
{
  mMessage += ": ";
  mMessage += strerror(errno);
}

const char*
IOException::what()
  throw()
{
  return mMessage.c_str();
}

ParserException::ParserException(const char* aMessage)
  throw()
  : mMessage(aMessage)
{
}

const char*
ParserException::what()
  throw()
{
  return mMessage.c_str();
}

void
ParserException::appendLocation(uint32_t line, uint32_t col)
{
  std::stringstream str;
  str << " at line " << line << ", column " << col;
  mMessage += str.str();
}

class BufferedTextSource
  : public TextSource
{
public:
  BufferedTextSource(const char* aFilename)
    : mFile(-1), mBuffer(NULL)
  {
    mFile = open(aFilename, O_RDONLY);
    if (mFile == -1)
      throw IOException("Can't open GenBank file");

    mBuffer = new char[1024];
    mReadPointer = mBuffer;
    mWritePointer = mBuffer;
    mEndBuffer = mBuffer + 1024;
  }

  ~BufferedTextSource()
  {
    if (mBuffer != NULL)
      delete [] mBuffer;
    if (mFile != -1)
      close(mFile);
  }
 
  char nextChar()
    throw(EndOfSource)
  {
    if (mReadPointer == mWritePointer)
    {
      mReadPointer = mWritePointer = mBuffer;
      mWritePointer += read(mFile, mReadPointer, mEndBuffer - mBuffer);
      if (mWritePointer < mReadPointer)
        throw IOException("Reading data");
      if (mReadPointer == mWritePointer)
        throw EndOfSource();
    }

    return *(mReadPointer++);
  }

  size_t getBlock(char** aBuf, uint32_t len)
    throw(EndOfSource)
  {
    if (mReadPointer == mWritePointer)
    {
      mReadPointer = mWritePointer = mBuffer;
      mWritePointer += read(mFile, mReadPointer, mEndBuffer - mBuffer);
      if (mWritePointer < mReadPointer)
        throw IOException("Reading data");
      if (mReadPointer == mWritePointer)
        throw EndOfSource();
    }

    uint32_t avail = mWritePointer - mReadPointer;

    if (avail > len)
      avail = len;

    *aBuf = mReadPointer;
    mReadPointer += avail;

    return avail;
  }

private:
  int mFile;
  char* mBuffer, * mEndBuffer, * mWritePointer, * mReadPointer;
};

TextSource* NewBufferedFileSource(const char* aFilename)
{
  return new BufferedTextSource(aFilename);
}

// mState is now a pointer-to-member, so we assign directly to the function...
#define START_OF_FILE &GenBankParserImpl::PCStartOfFile
#define KEYWORD_NAME &GenBankParserImpl::PCKeywordName
#define KEYWORD_DATA &GenBankParserImpl::PCKeywordData
#define AFTER_KEYWORD &GenBankParserImpl::PCAfterKeyword
#define SUBKEYWORD_NAME &GenBankParserImpl::PCSubkeywordName
#define SUBKEYWORD_DATA &GenBankParserImpl::PCSubkeywordData
#define AFTER_SUBKEYWORD &GenBankParserImpl::PCAfterSubkeyword
#define SUBSUBKEYWORD_NAME &GenBankParserImpl::PCSubsubkeywordName
#define SUBSUBKEYWORD_DATA &GenBankParserImpl::PCSubsubkeywordData
#define AFTER_SUBSUBKEYWORD &GenBankParserImpl::PCAfterSubsubkeyword
#define START_CODING_DATA &GenBankParserImpl::PCStartCodingData
#define START_FEATURE_DATA &GenBankParserImpl::PCStartFeatureData
#define CODING_SLASH2 &GenBankParserImpl::PCCodingSlash2
#define EXPECT_EOF_THEN_START &GenBankParserImpl::PCExpectEOFThenStart
#define DATA_OFFSET_NUMBER &GenBankParserImpl::PCDataOffsetNumber
#define CODING_SEQUENCE &GenBankParserImpl::PCCodingSequence
#define FEATURE_NAME &GenBankParserImpl::PCFeatureName
#define AFTER_FEATURE_NAME &GenBankParserImpl::PCAfterFeatureName
#define FEATURE_DATA &GenBankParserImpl::PCFeatureData
#define FEATURE_OR_QUALIFIER &GenBankParserImpl::PCFeatureOrQualifier
#define FEATURE_OR_QUALIFIER_OR_FEATURECONT &GenBankParserImpl::PCFeatureOrQualifierOrFeatureCont
#define FEATURE_OR_QUALIFIER_OR_QUALIFIERCONT &GenBankParserImpl::PCFeatureOrQualifierOrQualifierCont
#define QUALIFIER_NAME &GenBankParserImpl::PCQualifierName
#define QUALIFIER_OPEN_QUOTE &GenBankParserImpl::PCQualifierOpenQuote
#define QUALIFIER_DATA &GenBankParserImpl::PCQualifierData
#define QUALIFIER_UNQUOTED_DATA &GenBankParserImpl::PCQualifierUnquotedData
#define QUALIFIER_CONTINUATION &GenBankParserImpl::PCQualifierContinuation
#define QUALIFIER_NEWLINE &GenBankParserImpl::PCQualifierNewline

class GenBankParserImpl
  : public GenBankParser
{
public:
  GenBankParserImpl()
    : mBuf(NULL), mSink(NULL), mSource(NULL),
      mState(START_OF_FILE), mLine(1), mCol(0)
  {
  }

  ~GenBankParserImpl()
  {
  }

  void SetSource(TextSource* aSource)
  {
    mSource = aSource;
  }

  void SetSink(GenBankSink* aSink)
  {
    mSink = aSink;
  }

  void Parse()
  {
    if (mSink == NULL)
      throw ParserException("No sink set");
    if (mSource == NULL)
      throw ParserException("No source set");

    char* buf;
    uint32_t len;

    try
    {
      while (true)
      {
        mBufLen = mSource->getBlock(&mBuf, 1024);
        while (mBufLen > 0)
        {
          char c = *mBuf++;
          mBufLen--;

          if (c == '\n')
          {
            mLine++;
            mCol = 0;
          }
          else
            mCol++;

          (this->*mState)(c);
        }
      }
    }
    catch (EndOfSource eos)
    {
      mBuf = NULL;

      EndOfFile();
    }
    catch (ParserException pe)
    {
      mBuf = NULL;

      pe.appendLocation(mLine, mCol);
      throw pe;
    }
  }

  void
  ParseCharacter(char c)
  {
    if (c == '\n')
    {
      mLine++;
      mCol = 0;
    }
    else
      mCol++;

    try
    {
      (this->*mState)(c);
    }
    catch (ParserException pe)
    {
      pe.appendLocation(mLine, mCol);
      throw pe;
    }
  }

  void
  EndOfFile()
  {
    if (mState == START_OF_FILE)
      return;

    if (mStripSpaces == 0 &&
        ((mState == AFTER_KEYWORD) || (mState == AFTER_SUBKEYWORD) ||
         (mState == AFTER_SUBSUBKEYWORD)))
      return;

    throw ParserException("Unexpected end of file");
  }

private:
  char* mBuf;
  uint32_t mBufLen;

  GenBankSink* mSink;
  TextSource* mSource;
  uint32_t mLine, mCol;

  void (GenBankParserImpl::* mState)(char);

  enum
  {
    NOT_SPECIAL,
    FEATURES,
    CODING
  } mSpecialKeywordState;

  std::string mKeyword, mKeywordData;
  uint32_t mStripSpaces;

  void PCStartOfFile(char c)
  {
    if (c <= 'A' || c >= 'Z')
      ParserException("Invalid character at start of file");

    mKeyword = c;
    mState = KEYWORD_NAME;
  }
  
  void PCKeywordName(char c)
  {
    if (c == ' ')
    {
      if (mKeyword == "ORIGIN")
        mSpecialKeywordState = CODING;
      else if (mKeyword == "FEATURES")
        mSpecialKeywordState = FEATURES;
      else
        mSpecialKeywordState = NOT_SPECIAL;

      mState = KEYWORD_DATA;
      mKeywordData = "";
      mStripSpaces = 11 - mKeyword.size();
      return;
    }

    if (c == '\n')
    {
      if (mKeyword == "ORIGIN")
        mState = START_CODING_DATA;
      else if (mKeyword == "FEATURES")
        mState = START_FEATURE_DATA;
      else
        mState = AFTER_KEYWORD;

      mStripSpaces = 0;
      return;
    }

    mKeyword += c;
  }

  void PCKeywordData(char c)
  {
    if (c == '\n')
    {
      if (mSpecialKeywordState == NOT_SPECIAL)
        mState = AFTER_KEYWORD;
      else if (mSpecialKeywordState == FEATURES)
        mState = START_FEATURE_DATA;
      else
        mState = START_CODING_DATA;

      mStripSpaces = 0;
      return;
    }

    if (mStripSpaces != 0)
    {
      if (c != ' ')
        throw ParserException("Expected more spaces after keyword");
      mStripSpaces--;
      return;
    }

    mKeywordData += c;
  }

  void PCAfterKeyword(char c)
  {
    if (c == ' ')
    {
      mStripSpaces++;
      return;
    }

    if (c == '\n')
      throw ParserException("Unexpected blank line after keyword");

    if (mStripSpaces == 12)
    {
      mStripSpaces = 0;
      mState = KEYWORD_DATA;
      mKeywordData += ' ';
      PCKeywordData(c);
      return;
    }

    // This is the end of the keyword data, although there may still be
    // subkeywords...
    mSink->OpenKeyword(mKeyword.c_str(), mKeywordData.c_str());

    if (mStripSpaces == 0)
    {
      mSink->CloseKeyword();

      PCStartOfFile(c);
      return;
    }

    if (mStripSpaces != 2)
      throw ParserException("Unexpected number of spaces following keyword line");

    mState = SUBKEYWORD_NAME;
    mKeyword = c;
  }

  void PCSubkeywordName(char c)
  {
    if (c == ' ')
    {
      mState = SUBKEYWORD_DATA;
      mKeywordData = "";
      mStripSpaces = 9 - mKeyword.size();
      return;
    }

    if (c == '\n')
    {
      mState = AFTER_SUBKEYWORD;
      mStripSpaces = 0;
      return;
    }

    mKeyword += c;
  }

  void PCSubkeywordData(char c)
  {
    if (c == '\n')
    {
      mState = AFTER_SUBKEYWORD;
      mStripSpaces = 0;
      return;
    }

    if (mStripSpaces != 0)
    {
      if (c != ' ')
        throw ParserException("Expected more spaces after sub-keyword");
      mStripSpaces--;
      return;
    }

    mKeywordData += c;
  }

  void PCAfterSubkeyword(char c)
  {
    if (c == ' ')
    {
      mStripSpaces++;
      return;
    }

    if (c == '\n')
      throw ParserException("Unexpected blank line after sub-keyword");

    if (mStripSpaces == 12)
    {
      mStripSpaces = 0;
      mState = SUBKEYWORD_DATA;
      mKeywordData += ' ';
      PCSubkeywordData(c);
      return;
    }

    // This is the end of the subkeyword data, although there may still be
    // sub-subkeywords...
    mSink->OpenKeyword(mKeyword.c_str(), mKeywordData.c_str());

    if (mStripSpaces == 0)
    {
      mSink->CloseKeyword();
      mSink->CloseKeyword();

      PCStartOfFile(c);
      return;
    }
    else if (mStripSpaces == 2)
    {
      mSink->CloseKeyword();

      mKeyword = c;
      mState = SUBKEYWORD_NAME;
      return;
    }

    if (mStripSpaces != 3)
      throw ParserException("Unexpected number of spaces following subkeyword line");

    mState = SUBSUBKEYWORD_NAME;
    mKeyword = c;
  }

  void PCSubsubkeywordName(char c)
  {
    if (c == ' ')
    {
      mState = SUBSUBKEYWORD_DATA;
      mKeywordData = "";
      mStripSpaces = 8 - mKeyword.size();
      return;
    }

    if (c == '\n')
    {
      mState = AFTER_SUBSUBKEYWORD;
      mStripSpaces = 0;
      return;
    }

    mKeyword += c;
  }

  void PCSubsubkeywordData(char c)
  {
    if (c == '\n')
    {
      mState = AFTER_SUBSUBKEYWORD;
      mStripSpaces = 0;
      return;
    }

    if (mStripSpaces != 0)
    {
      if (c != ' ')
        throw ParserException("Expected more spaces after sub-sub-keyword");
      mStripSpaces--;
      return;
    }

    mKeywordData += c;
  }

  void PCAfterSubsubkeyword(char c)
  {
    if (c == ' ')
    {
      mStripSpaces++;
      return;
    }

    if (c == '\n')
      throw ParserException("Unexpected blank line after sub-sub-keyword");

    if (mStripSpaces == 12)
    {
      mStripSpaces = 0;
      mState = SUBSUBKEYWORD_DATA;
      mKeywordData += ' ';
      PCSubsubkeywordData(c);
      return;
    }

    // This is the end of the subsubkeyword data.
    mSink->OpenKeyword(mKeyword.c_str(), mKeywordData.c_str());

    if (mStripSpaces == 0)
    {
      mSink->CloseKeyword();
      mSink->CloseKeyword();
      mSink->CloseKeyword();

      PCStartOfFile(c);
      return;
    }
    else if (mStripSpaces == 2)
    {
      mSink->CloseKeyword();
      mSink->CloseKeyword();

      mKeyword = c;
      mState = SUBKEYWORD_NAME;
      return;
    }

    if (mStripSpaces != 3)
      throw ParserException("Unexpected number of spaces following subkeyword line");

    mSink->CloseKeyword();
    mState = SUBSUBKEYWORD_NAME;
    mKeyword = c;
  }

  void PCStartCodingData(char c)
  {
    if (c == ' ')
    {
      mStripSpaces++;
      return;
    }
    if (mStripSpaces == 0 && c == '/')
    {
      mState = CODING_SLASH2;
      return;
    }

    mState = DATA_OFFSET_NUMBER;
  }

  void PCCodingSlash2(char c)
  {
    if (c != '/')
      throw ParserException("Expected / after /");
    mState = EXPECT_EOF_THEN_START;
  }

  void PCExpectEOFThenStart(char c)
  {
    if (c != '\n')
      throw ParserException("Expected newline after //");
    mState = START_OF_FILE;
  }

  void PCDataOffsetNumber(char c)
  {
    if (c == ' ')
    {
      mKeywordData = "";
      mState = CODING_SEQUENCE;
    }
    else if (c >= '0' && c <= '9')
      ;
    else
      throw ParserException("Expected a digit in base offset");
  }

  void PCCodingSequence(char c)
  {
    if (UNLIKELY(c <= ' '))
    {
      if (c == ' ')
        return;
      if (c == '\n')
      {
        mSink->CodingData(mKeywordData.c_str());
        mState = START_CODING_DATA;
        mStripSpaces = 0;
        return;
      }
    }
    mKeywordData += c;

    if (mBuf == NULL)
      return;
    
    // We break our standard practice here and short-circuit the rest of the
    // parsing machinery here because this is the single most common code path.

    while (LIKELY(mBufLen > 0 && *mBuf != '\n'))
    {
      c = *mBuf++;
      mBufLen--;
      
      if (UNLIKELY(c == ' '))
        continue;

      mKeywordData += c;
    }
  }

  void PCStartFeatureData(char c)
  {
    if (c == ' ')
    {
      mStripSpaces++;
      return;
    }

    if (mStripSpaces == 0)
    {
      PCStartOfFile(c);
      return;
    }

    if (mStripSpaces != 5)
      throw ParserException("Unexpected number of spaces at the end of feature name");

    mKeyword = c;
    mState = FEATURE_NAME;
  }

  void PCFeatureName(char c)
  {
    if (c == ' ')
    {
      mStripSpaces = 21 - 5 - mKeyword.size() - 1;
      mKeywordData = "";
      mState = AFTER_FEATURE_NAME;
      return;
    }
    else if (c == '\n')
      throw ParserException("Unexpected end-of-line in feature name");

    mKeyword += c;
  }

  void PCAfterFeatureName(char c)
  {
    if (c != ' ')
      throw ParserException("Incorrect number of spaces after feature name");

    mStripSpaces--;

    if (mStripSpaces == 0)
    {
      mState = FEATURE_DATA;
      return;
    }
  }

  void PCFeatureData(char c)
  {
    if (c == '\n')
    {
      mStripSpaces = 0;
      mState = FEATURE_OR_QUALIFIER_OR_FEATURECONT;
      return;
    }

    mKeywordData += c;
  }

  void PCFeatureOrQualifierOrFeatureCont(char c)
  {
    if (c == ' ')
    {
      mStripSpaces++;
      return;
    }

    if (mStripSpaces == 0)
    {
      mSink->CloseFeature();
      mSink->CloseKeyword();
      PCStartOfFile(c);
      return;
    }

    if (mStripSpaces == 5)
    {
      mSink->OpenFeature(mKeyword.c_str(), mKeywordData.c_str());
      mSink->CloseFeature();

      mState = FEATURE_NAME;
      mKeyword = c;
      return;
    }

    if (mStripSpaces != 21)
      throw ParserException("Incorrect number of spaces on line following feature");

    if (c == '/')
    {
      mSink->OpenFeature(mKeyword.c_str(), mKeywordData.c_str());
      mState = QUALIFIER_NAME;
      return;
    }
    
    mState = FEATURE_DATA;
    PCFeatureData(c);
  }

  void PCFeatureOrQualifier(char c)
  {
    if (c == ' ')
    {
      mStripSpaces++;
      return;
    }

    if (mStripSpaces == 0)
    {
      mSink->CloseFeature();
      mSink->CloseKeyword();
      PCStartOfFile(c);
      return;
    }

    if (mStripSpaces == 5)
    {
      mSink->CloseFeature();

      mState = FEATURE_NAME;
      mKeyword = c;
      return;
    }

    if (mStripSpaces != 21)
      throw ParserException("Incorrect number of spaces on line following feature");

    if (c != '/')
      throw ParserException("Expected qualifier to start with a slash");
    
    mState = QUALIFIER_NAME;
  }

  void PCFeatureOrQualifierOrQualifierCont(char c)
  {
    if (c == ' ')
    {
      mStripSpaces++;
      return;
    }

    if (mStripSpaces == 0)
    {
      mSink->Qualifier(mKeyword.c_str(), mKeywordData.c_str());
      mSink->CloseFeature();
      mSink->CloseKeyword();
      PCStartOfFile(c);
      return;
    }

    if (mStripSpaces == 5)
    {
      mSink->Qualifier(mKeyword.c_str(), mKeywordData.c_str());
      mSink->CloseFeature();

      mState = FEATURE_NAME;
      mKeyword = c;
      return;
    }

    if (mStripSpaces != 21)
      throw ParserException("Incorrect number of spaces on line following feature");

    if (c != '/')
    {
      mState = QUALIFIER_UNQUOTED_DATA;
      PCQualifierUnquotedData(c);
      return;
    }

    mSink->Qualifier(mKeyword.c_str(), mKeywordData.c_str());    
    mState = QUALIFIER_NAME;
  }

  void PCQualifierName(char c)
  {
    if (c == ' ')
      throw ParserException("Unexpected whitespace in feature name");

    if (c == '\n')
    {
      PCQualifierUnquotedData(c);
      return;
    }

    if (c == '=')
    {
      mState = QUALIFIER_OPEN_QUOTE;
      return;
    }

    mKeyword += c;
  }

  void PCQualifierOpenQuote(char c)
  {
    if (c != '"')
    {
      mState = QUALIFIER_UNQUOTED_DATA;
      mKeywordData = "";
      return;
    }

    mKeywordData = "";
    mState = QUALIFIER_DATA;
  }

  void PCQualifierData(char c)
  {
    if (c == '"')
    {
      mState = QUALIFIER_NEWLINE;
      mSink->Qualifier(mKeyword.c_str(), mKeywordData.c_str());
      return;
    }

    if (c == '\n')
    {
      mState = QUALIFIER_CONTINUATION;
      mStripSpaces = 21;
      return;
    }

    mKeywordData += c;
  }

  void PCQualifierUnquotedData(char c)
  {
    if (c == '\n')
    {
      mStripSpaces = 0;
      mState = FEATURE_OR_QUALIFIER_OR_QUALIFIERCONT;
      return;
    }

    mKeywordData += c;
  }

  void PCQualifierContinuation(char c)
  {
    if (c != ' ')
      throw ParserException("Unterminated qualifier value");

    mStripSpaces--;

    if (mStripSpaces == 0)
    {
      mKeywordData += ' ';
      mState = QUALIFIER_DATA;
    }
  }

  void PCQualifierNewline(char c)
  {
    if (c != '\n')
      throw ParserException("Garbage at end of qualifier line");
    
    mStripSpaces = 0;
    mState = FEATURE_OR_QUALIFIER;
  }
};

GenBankParser*
NewGenBankParser()
{
  return new GenBankParserImpl();
}
